Uniqueness, custom types and storage maps
In this part of the tutorial, we'll build out the components of our pallet needed to manage the creation and ownership of our Kitties.
#
OverviewThis part of the tutorial dives into some pillar concepts for developing pallets with FRAME. Ontop of learning how to use existing types and traits, you'll learn how create your own types like providing your pallet with a Gender type. At the end of this part, you will have implemented the 2 remaining storage items according to the logic outlined for the Substrate Kitty application in the overview of this tutorial.
#
Learning outcomes➡️ Writing a custom struct and using it in storage.
➡️ Creating a custom type and implementing it for your pallet's Config
trait.
➡️ Using the Randomness trait in a helper function.
➡️ Adding StorageMap
items to a pallet.
#
Steps#
1. Kitty struct scaffoldingWe added additional comments to the code from Part I (in the /pallets/mykitties/src/lib.rs
file) to better assist you with the action items in this part of the tutorial. To follow each step with ease,
you can just replace your code with the helper code provided below:
note
If you're feeling confident, you can use the code from the previous part and use the comments marked as "TODO" to follow each step instead of pasting in the helper file for this part.
#
A. What information to includeStructs are a useful tool to help store data that have things in common. For our purposes, our Kitty will carry multiple traits which we can store in a single struct instead of using separate storage items. This comes in handy when trying to optimize for storage reads and writes because our runtime will need to perform less read/writes to update multiple values. Read more about storage best practices here.
Let's first go over what information a single Kitty will carry:
dna
: the hash used to identify the DNA of a Kitty, which corresponds to its unique features. DNA is also used to breed new Kitties and to keep track of different Kitty generations.price
: this is a balance that corresponds to the amount needed to buy a Kitty and set by its owner.gender
: an enum that can be eitherMale
orFemale
.owner
: an account ID designating a single owner.
#
B. Sketching out the types held by our structLooking at the items of our struct from step 1A, we can deduce the following types:
[u8; 16]
fordna
(to use 16 bytes to represent a Kitty's DNA)BalanceOf
forprice
(this is a custom type using FRAME'sCurrency
trait)Gender
forgender
(we're going to need to create this!)
First, we'll need to add in our custom types for BalanceOf
and AccountOf
before we declare our struct. Replace ACTION #1 with the following snippet:
Now, paste in the our Kitty struct:
Notice how we use the derive macro to include various helper traits for using our struct.
For type Gender
, we'll need to build out our own custom enum and helper functions. Now's a good time
to do that.
Gender
#
2. Writing a custom type for We've just created a struct that requires a custom type called Gender
. This type will handle an enum defining our Kitty's gender. To create it, you'll build out the following parts:
- An enum declaration, which specifies
Male
andFemale
values. - A function to configure a defaut value, based on the enum.
info
Setting up our Gender
enum using a function to configure its default value will allow us to derive a Kitty's gender by passing in the randomness
created by each Kitty's DNA.
#
A. EnumsReplace ACTION item #2 with the following enum declaration:
Notice the use of the derive macro which must precede the enum declaration. This wraps our enum in the data structures it will need to interface with other types in our runtime.
Then, we need a function to define a default implementation for our enum by using Rust's Default
trait.
Replace the line containing ACTION #3 with:
💡 This is like saying: let's give our enum a special trait that allows us to initialize it to a specific value.
Great, we now know how to create a custom struct and specify its default value. But what about providing a way for a Kitty struct to be assigned a gender value? For that we need to learn one more thing.
#
B. Configuring functions for our Kitty structConfiguring a struct is useful in order to pre-define a value in our struct. For example, when setting
a value in relation to what another function returns. In our case we have a similar situation where
we need to configure our Kitty struct in such a way that sets Gender
according to a Kitty's DNA.
We'll only be using this function when we get to creating Kitties. Regardless, learn how to write it now and get it out of the way.
When you're implementing the configuration trait for a struct inside a FRAME pallet, you're doing the
same type of thing as implementing some trait for an enum except you're implementing the generic
configuration trait, Config
. In our case we'll create a public function called gen_gender
that returns the Gender
type
and uses a random function to choose between Gender
enum values.
Replace ACTION #4 with the following code snippet:
Now whenever gen_gender()
is called inside our pallet, it will return a pseudo random enum value for Gender
.
#
3. Implement on-chain randomnessIf we want to be able to tell these Kitties apart, we need to start giving them unique properties!
In the previous step, we've made use of KittyRandomness
which we haven't actually defined yet. Let's get to it.
We'll be using the Randomness trait from frame_support
to do this. It will be able to generate a random seed which
we'll create unique Kitties with as well as breed new ones.
In order to implement the Randomness
trait for our runtime, we must:
A. Specify it in our pallet's configuration trait.
The Randomness
trait from frame_support
requires specifying it with a paramater to replace the Output
and BlockNumber
generics.
Take a look at the documentation and the source code implementation to understand how this works. For our purposes,
we want the output of functions using this trait to be H256
which you'll notice should already be declared at the
top of your working codebase.
Replace the ACTION #5 line with:
B. Specify it for our runtime.
Given that we're adding a new type for the configuration of our pallet, we need to tell our runtime about its implementation.
This could come in handy if ever we wanted to change the algorithm that KittyRandomness
is using, without needing to
modify where it's used inside our pallet.
To showcase this point, we're going to implement KittyRandomness
by assigning it to an instance of FRAME's RandomnessCollectiveFlip
.
Conveniently, the Node Template already has an instance of the RandomnessCollectiveFlip
pallet.
All you need to do is include the KittyRandomness
type for your runtime inside runtime/src/lib.rs
:
tip
Check out this how-to guide on implementing randomness in case you get stuck.
#
Generating random DNAGenerating DNA is similar to using randomness to randomly assign a gender type. The difference is that we'll be making use of blake2_128
we imported in the previous part. Replace the ACTION #7 line with:
#
4. Write remaining storage items#
A. Understanding storage item logicTo easily track all of our kitties, we're going to standardize our logic to use a unique ID as the global key for our storage items. This means that a single unique key will point to our Kitty object (i.e. the struct we previously declared).
In order for this to work, we need to make sure that the ID for a new Kitty is always unique.
We can do this with a new storage item Kitties
which will be a mapping from an ID (Hash) to the Kitty object.
With this object, we can easily check for collisions by simply checking whether this storage item already contains a mapping using a particular ID. For example, from inside a dispatchable function we could check using:
tip
Our pallet's logic can best be understood by examining the storage items we'll be using. In other words, the way we define the conditions for reading and writing to our runtime's storage helps us breakdown the items we'll need to enable NFT capabilities.
We care about state transitions and persistance around two main concepts our runtime needs to be made aware of:
- Unique assets, like currency or Kitties (this will be held by a storgae map called
Kitties
) - Ownership of those assets, like account IDs (this will be handled by
KittyCnt
and a new storage map calledKittiesOwned
)
StorageMap
#
B. Using a To create a storage instance for the Kitty struct,
we'll be using StorageMap
— a hash-map provided
to us by FRAME.
Here's what the Kitties
storage item looks like:
Breaking it down, we declare the storage type and assign a StorageMap
that takes:
- The
Twox64Concat
hashing algorithm. - A key of type
T::Hash
. - A value of type
Kitty<T>
.
The KittiesOwned
storage item is similar except that we'll be using a BoundedVec
to keep track of some maximum number of Kitties we'll configure in runtime/src/lib.s
.
Your turn! Copy the two code snippets above to replace line ACTION #8.
Before we can check our pallet compiles, we need to add MaxKittyOwned
, which is a pallet constant that we need to declare (similar to KittyRandomness
in the previous steps). Replace ACTION #9 with:
And add this type to runtime/src/lib.rs
:
Assuming you've followed the above steps correctly, now's a good time to check that your pallet compiles:
Running into difficulties? Check your solution against the completed helper code for this part of the tutorial.
Congratulations!
If you've made it this far, you now have the foundations for your pallet to handle the creation and changes in ownership of your Kitties! In this part of the tutorial, we've learnt:
- How to write a struct and use it in a
StorageMap
. - How to implement a custom type.
- How to set a default enum value for a custom type.
- How to create a function to set a value for that custom type.
- How to implement the Randomness trait to write a function that generates randomness using a nonce.
- How to write
StorageMap
storage items.
#
Next steps- Create a dispatchable function that mints a new Kitty
- Create a helper function to handle storage updates
- Implement Errors and Events